<template>
  <div class="iot-monitor-container">
    <div class="app-container">
      <el-row :gutter="12">
        <el-col :span="7">
          <el-card>
            <div style="display:flex; gap:8px; align-items:center; margin-bottom:8px">
              <el-input v-model="devQuery.keyword" placeholder="搜索设备名称/编码" clearable />
              <el-button type="primary" @click="loadDevices">查询</el-button>
            </div>
            <el-table :data="devices" height="520" @row-click="selectDevice" highlight-current-row :current-row-key="currentDevice?.deviceId" row-key="deviceId" :key="onlineTick">
              <el-table-column prop="deviceName" label="设备" min-width="140">
                <template #default="{ row }">
                  <div style="display:flex; align-items:center; gap:6px">
                    <el-badge :is-dot="true" :type="calcOnline(row) ? 'success' : 'danger'" />
                    <div>
                      <div>{{ row.deviceName || '-' }}</div>
                      <div style="font-size:12px;color:#909399">{{ row.deviceCode }}</div>
                    </div>
                  </div>
                </template>
              </el-table-column>
              <el-table-column prop="protocolType" label="协议" width="90" />
            </el-table>
            <div style="margin-top:8px; text-align:right">
              <el-pagination
                background
                layout="total, prev, pager, next"
                v-model:current-page="devQuery.pageNum"
                v-model:page-size="devQuery.pageSize"
                :total="devTotal"
                @current-change="loadDevices"
              />
            </div>
          </el-card>
        </el-col>

        <el-col :span="17">
          <el-tabs v-model="activeTab">
            <el-tab-pane label="设备监控" name="monitor">
              <el-card>
                <template #header>
                  <div style="display:flex; justify-content:space-between; align-items:center">
                    <div>
                      <span style="font-weight:600">设备状态</span>
                      <span v-if="currentDevice" style="margin-left:8px;color:#606266">{{ currentDevice.deviceName }}（{{ currentDevice.deviceCode }}）</span>
                    </div>
                    <div style="display:flex; gap:8px; align-items:center">
                      <el-input v-model="pointFilter" placeholder="点位编码(可选)" clearable style="width:200px" />
                      <el-switch v-model="autoRefresh" active-text="自动刷新" @change="toggleAuto" />
                      <el-button @click="refreshDetail">刷新</el-button>
                    </div>
                  </div>
                </template>

                <el-empty v-if="!currentDevice" description="请选择左侧设备" />

                <div v-else>
                  <div style="display:flex; gap:16px; margin-bottom:12px">
                    <el-statistic title="在线状态" :value="calcOnline(currentDevice) ? 1 : 0" :precision="0">
                      <template #suffix>
                        <span style="font-size: 14px; color: #606266;">
                          {{ calcOnline(currentDevice) ? '在线' : '离线' }}
                        </span>
                      </template>
                    </el-statistic>
                    <el-statistic title="最近记录数" :value="records.length" :precision="0" />
                    <div style="display: flex; flex-direction: column; justify-content: center;">
                      <div style="font-size: 12px; color: #909399; margin-bottom: 4px;">最近上报时间</div>
                      <div style="font-size: 18px; font-weight: 600; color: #303133;">
                        {{ lastReportTime || '-' }}
                      </div>
                    </div>
                  </div>

                  <div ref="chartRef" style="height:280px; width:100%; margin-bottom:12px"></div>

                  <el-table :data="records" border stripe height="380">
                    <el-table-column prop="pointId" label="点位ID" width="120" />
                    <el-table-column prop="dataValue" label="值" />
                    <el-table-column prop="dataTime" label="时间" width="180" />
                    <el-table-column prop="quality" label="质量" width="100" />
                  </el-table>
                </div>
              </el-card>
            </el-tab-pane>

            <el-tab-pane label="远程控制" name="control">
              <el-card header="远程控制">
                <div class="control-section">
                  <div class="section-title">MQTT 发布</div>
                  <el-form :model="mqttForm" label-width="90px" size="small">
                    <el-form-item label="Topic">
                      <el-input v-model="mqttForm.topic" placeholder="devices/{code}/cmd" />
                    </el-form-item>
                    <el-form-item label="Payload">
                      <el-input v-model="mqttForm.payload" type="textarea" :rows="3" placeholder='{"switch":1}' />
                    </el-form-item>
                    <el-form-item>
                      <el-button type="primary" @click="onMqttPublish" v-hasPermi="['iot:control:execute']">发布</el-button>
                    </el-form-item>
                  </el-form>
                </div>
                <el-divider />
                <div class="control-section">
                  <div class="section-title">Modbus 读保持寄存器</div>
                  <el-form :model="modbusReadForm" label-width="110px" size="small">
                    <el-form-item label="协议">
                      <el-select v-model="modbusReadForm.protocol" style="width: 120px">
                        <el-option label="TCP" value="TCP" />
                        <el-option label="RTU" value="RTU" />
                      </el-select>
                    </el-form-item>
                    <el-form-item label="站号(slaveId)">
                      <el-input-number v-model="modbusReadForm.slaveId" :min="0" />
                    </el-form-item>
                    <el-form-item label="寄存器地址(offset)">
                      <el-input-number v-model="modbusReadForm.offset" :min="0" />
                    </el-form-item>
                    <el-form-item>
                      <el-button type="primary" @click="onModbusRead" v-hasPermi="['iot:control:execute']">读取</el-button>
                      <span style="margin-left:12px;color:#606266">读取值：{{ modbusReadValue }}</span>
                    </el-form-item>
                  </el-form>
                </div>
                <el-divider />
                <div class="control-section">
                  <div class="section-title">Modbus 写保持寄存器</div>
                  <el-form :model="modbusForm" label-width="110px" size="small">
                    <el-form-item label="协议">
                      <el-select v-model="modbusForm.protocol" style="width: 120px">
                        <el-option label="TCP" value="TCP" />
                        <el-option label="RTU" value="RTU" />
                      </el-select>
                    </el-form-item>
                    <el-form-item label="站号(slaveId)">
                      <el-input-number v-model="modbusForm.slaveId" :min="0" />
                    </el-form-item>
                    <el-form-item label="寄存器地址(offset)">
                      <el-input-number v-model="modbusForm.offset" :min="0" />
                    </el-form-item>
                    <el-form-item label="写入值(value)">
                      <el-input-number v-model="modbusForm.value" :min="0" />
                    </el-form-item>
                    <el-form-item>
                      <el-button type="success" @click="onModbusWrite" v-hasPermi="['iot:control:execute']">写入</el-button>
                    </el-form-item>
                  </el-form>
                </div>
              </el-card>
            </el-tab-pane>
          </el-tabs>
        </el-col>
      </el-row>
    </div>
    
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted, onBeforeUnmount, watch, nextTick, onDeactivated } from 'vue'
import { iotDevicePage, iotListRecords } from '@/api/iot'
import { ElMessage, ElMessageBox } from 'element-plus'
import { controlMqttPublish, controlModbusWrite } from '@/api/iot'
import { modbusTcpRead, modbusRtuRead } from '@/api/iot'
import { initWebSocket } from '@/utils/websocket'

// 定义组件名称，用于 keep-alive
defineOptions({
  name: 'IotMonitor'
})

const activeTab = ref<'monitor' | 'control'>('monitor')

const ONLINE_THRESHOLD_MS = (() => {
  const envAny: any = (import.meta as any).env || {}
  const sec = Number(envAny.VITE_APP_ONLINE_THRESHOLD_SEC || 120)
  return (isNaN(sec) || sec <= 0 ? 120 : sec) * 1000
})()

const devQuery = reactive({ pageNum: 1, pageSize: 10, keyword: '' })
const devices = ref<any[]>([])
const devTotal = ref(0)
const currentDevice = ref<any>(null)
const onlineTick = ref(0)

const records = ref<any[]>([])
const lastReportTime = ref<string>('')
const pointFilter = ref<string>('')

const autoRefresh = ref(true)
let timer: any = null

// 远程控制表单（MQTT / Modbus）
const mqttForm = reactive({
  topic: '',
  payload: ''
})

const modbusForm = reactive({
  protocol: 'TCP' as 'TCP'|'RTU',
  slaveId: 1,
  offset: 0,
  value: 0
})

const modbusReadForm = reactive({
  protocol: 'TCP' as 'TCP'|'RTU',
  slaveId: 1,
  offset: 0
})
const modbusReadValue = ref<string>('')

// chart
const chartRef = ref<HTMLElement | null>(null)
let chart: any = null
let echarts: any = null

async function ensureChart() {
  if (!echarts) {
    echarts = await import('echarts')
  }
  await nextTick()
  if (chartRef.value && !chart) {
    chart = echarts.init(chartRef.value)
    window.addEventListener('resize', handleResize)
  }
}

function handleResize() {
  if (chart) chart.resize()
}

function disposeChart() {
  if (chart) {
    window.removeEventListener('resize', handleResize)
    chart.dispose()
    chart = null
  }
}

function updateChart() {
  if (!chart) return
  // 过滤点位
  const data = (records.value || [])
    .filter(r => !pointFilter.value || String(r.pointCode || '').includes(pointFilter.value))
    .slice()
    .reverse()

  const x = data.map(d => d.dataTime)
  const y = data.map(d => {
    const v = Number(d.dataValue)
    return isNaN(v) ? null : v
  })

  const option = {
    tooltip: { trigger: 'axis' },
    grid: { left: 40, right: 20, top: 20, bottom: 40 },
    xAxis: { type: 'category', data: x, boundaryGap: false, axisLabel: { hideOverlap: true } },
    yAxis: { type: 'value', scale: true },
    series: [
      { type: 'line', data: y, smooth: true, showSymbol: false, connectNulls: true, areaStyle: { opacity: 0.08 } }
    ]
  }
  chart.setOption(option)
}

async function loadDevices() {
  try {
    const response = await iotDevicePage(devQuery)
    const data = response?.data || response
    const list = data?.records || data?.rows || data?.list || []
    devices.value = list
    // 映射按 deviceCode 缓存到 deviceId
    for (const d of list) {
      const code = d?.deviceCode
      if (code && latestByCode[code]) {
        const ts = latestByCode[code]
        deviceLatestObj[String(d.deviceId)] = ts
        ;(d as any)._onlineTs = ts
        onlineTick.value++
      }
    }
    // 触发表格渲染
    devices.value = devices.value.slice()
    devTotal.value = data?.total || data?.totalCount || 0
    if (!currentDevice.value && list.length) {
      currentDevice.value = list[0]
      await refreshDetail()
    }
  } catch (e: any) {
    console.error('加载设备数据失败：', e)
    ElMessage.error('加载设备数据失败：' + (e?.message || '未知错误'))
  }
}

function selectDevice(row: any) {
  currentDevice.value = row
  refreshDetail()
}

function calcOnline(d: any) {
  if (!d) return false
  // 优先使用行内最近在线时间，确保行级别响应式
  const inline = (d as any)._onlineTs
  const ts = inline || deviceLatestObj[String(d.deviceId)]
  if (!ts) return false
  return Date.now() - new Date(ts).getTime() < ONLINE_THRESHOLD_MS
}
function getRowDebug(d: any) {
  const inline = (d as any)._onlineTs
  const globalTs = deviceLatestObj[String(d.deviceId)]
  const ts = inline || globalTs
  const age = ts ? Math.round((Date.now() - new Date(ts).getTime()) / 1000) : null
  return `ts=${ts || '-'} age=${age ?? '-'}s inline=${inline ? 'Y' : 'N'} global=${globalTs ? 'Y' : 'N'}`
}

// 使用响应式对象而非 Map，确保视图能追踪到键值变化
const deviceLatestObj = reactive<Record<string, string>>({})
// websocket 可能先于设备列表到达，这里按 deviceCode 暂存，列表加载后再映射到 deviceId
const latestByCode = reactive<Record<string, string>>({})

async function refreshDetail() {
  if (!currentDevice.value) return
  const params: any = { pageNum: 1, pageSize: 200, deviceId: currentDevice.value.deviceId }
  if (pointFilter.value) params.pointCode = pointFilter.value
  const { data } = await iotListRecords(params)
  const list = data?.records || []
  records.value = list
  if (list.length) {
    const latest = list[0].dataTime
    lastReportTime.value = latest
    deviceLatestObj[String(currentDevice.value.deviceId)] = latest
    // 同步到当前行对象
    const row = devices.value.find(x => x.deviceId === currentDevice.value.deviceId)
    if (row) (row as any)._onlineTs = latest
    onlineTick.value++
  } else {
    lastReportTime.value = ''
    deviceLatestObj[String(currentDevice.value.deviceId)] = ''
    const row = devices.value.find(x => x.deviceId === currentDevice.value.deviceId)
    if (row) (row as any)._onlineTs = ''
    onlineTick.value++
  }
  // 触发表格渲染
  devices.value = devices.value.slice()
  await ensureChart()
  updateChart()
}

function toggleAuto(val: boolean) {
  if (val) {
    startTimer();
  } else {
    stopTimer();
  }
}

function startTimer() {
  stopTimer()
  timer = setInterval(() => {
    refreshDetail()
  }, 5000)
}

function stopTimer() {
  if (timer) { 
    clearInterval(timer)
    timer = null 
  }
}

// 清理所有资源
function cleanup() {
  stopTimer()
  disposeChart()
  // 清理数据
  devices.value = []
  records.value = []
  currentDevice.value = null
  lastReportTime.value = ''
  // 清空响应式缓存
  for (const k in deviceLatestObj) delete (deviceLatestObj as any)[k]
  for (const k in (latestByCode as any)) delete (latestByCode as any)[k]
  onlineTick.value++
}

watch(currentDevice, () => {
  if (autoRefresh.value) refreshDetail()
})

watch(pointFilter, () => {
  // 过滤条件变化，重拉并更新图表
  refreshDetail()
})

onMounted(async () => {
  // 先初始化 WebSocket，再加载设备，避免早到的消息丢失
  try {
    // 允许通过环境变量直接指定 WS 完整地址，避免后端经代理时无法推导
    const envAny: any = (import.meta as any).env || {}
    const wsEnv: string = envAny.VITE_APP_WS_URL || ''
    let wsUrl = ''
    if (wsEnv && (wsEnv.startsWith('ws://') || wsEnv.startsWith('wss://'))) {
      wsUrl = wsEnv
    } else {
      // 以后端 API 地址为准，转换为 ws(s) 协议
      const apiBase: string = envAny.VITE_APP_BASE_API || ''
      let wsOrigin = ''
      let basePath = ''
      if (apiBase.startsWith('http://') || apiBase.startsWith('https://')) {
        // 绝对地址：取其 origin
        const u = new URL(apiBase)
        wsOrigin = u.origin.startsWith('https') ? u.origin.replace('https', 'wss') : u.origin.replace('http', 'ws')
        basePath = ''
      } else if (apiBase.startsWith('/')) {
        // 相对代理路径：走前端 origin + 代理前缀
        wsOrigin = location.origin.startsWith('https') ? location.origin.replace('https', 'wss') : location.origin.replace('http', 'ws')
        basePath = apiBase
      } else {
        // 未配置或异常：退回前端 origin
        wsOrigin = location.origin.startsWith('https') ? location.origin.replace('https', 'wss') : location.origin.replace('http', 'ws')
        basePath = ''
      }
      // 规范化拼接路径
      const join = (a: string, b: string) => a.replace(/\/$/, '') + '/' + b.replace(/^\//, '')
      const baseWithPath = basePath ? join(wsOrigin, basePath) : wsOrigin
      wsUrl = join(baseWithPath, '/websocket/iot-monitor')
    }
    const ws = initWebSocket(wsUrl)
    // 透传监听到的消息，尝试解析设备状态事件
    if (ws) {
      ws.onmessage = (e: any) => {
        try {
          const msg = JSON.parse(e.data)
          if (msg && msg.type === 'device_online' && msg.deviceCode) {
            // 在设备列表中更新在线时间
            const d = devices.value.find(x => x.deviceCode === msg.deviceCode)
            if (d) {
              const ts = new Date(msg.timestamp || Date.now()).toISOString()
              deviceLatestObj[String(d.deviceId)] = ts
              ;(d as any)._onlineTs = ts
              onlineTick.value++
              // 触发表格渲染
              devices.value = devices.value.slice()
            } else {
              (latestByCode as any)[msg.deviceCode] = new Date(msg.timestamp || Date.now()).toISOString()
            }
            // 若当前就是这个设备，刷新明细
            if (currentDevice.value && currentDevice.value.deviceCode === msg.deviceCode) {
              refreshDetail()
            }
          }
        } catch { /* ignore non-JSON */ }
      }
    }
  } catch (e) {
    console.warn('WebSocket 初始化失败', e)
  }
  await loadDevices()
  if (autoRefresh.value) startTimer()
})

onBeforeUnmount(() => {
  cleanup()
})

// 添加 onDeactivated 钩子，当组件被缓存时调用
onDeactivated(() => {
  cleanup()
})

async function onMqttPublish() {
  if (!currentDevice.value) {
    ElMessage.error('请先选择设备')
    return
  }
  try {
    await ElMessageBox.confirm('确定发布该 MQTT 指令？', '确认', { type: 'warning' })
    await controlMqttPublish({
      topic: mqttForm.topic,
      payload: mqttForm.payload,
      deviceId: currentDevice.value.deviceId,
      deviceCode: currentDevice.value.deviceCode
    })
    ElMessage.success('发布成功')
  } catch (e: any) {
    if (e !== 'cancel') ElMessage.error(e?.msg || '发布失败')
  }
}

async function onModbusWrite() {
  if (!currentDevice.value) {
    ElMessage.error('请先选择设备')
    return
  }
  try {
    await ElMessageBox.confirm('确定执行 Modbus 写保持寄存器？', '确认', { type: 'warning' })
    await controlModbusWrite({
      protocol: modbusForm.protocol,
      slaveId: Number(modbusForm.slaveId),
      offset: Number(modbusForm.offset),
      value: Number(modbusForm.value),
      deviceId: currentDevice.value.deviceId,
      deviceCode: currentDevice.value.deviceCode
    })
    ElMessage.success('写入成功')
  } catch (e: any) {
    if (e !== 'cancel') ElMessage.error(e?.msg || '写入失败')
  }
}

async function onModbusRead() {
  if (!currentDevice.value) {
    ElMessage.error('请先选择设备')
    return
  }
  try {
    const base: any = { slaveId: Number(modbusReadForm.slaveId), offset: Number(modbusReadForm.offset), deviceId: currentDevice.value.deviceId }
    const res = modbusReadForm.protocol === 'TCP' ? await modbusTcpRead(base) : await modbusRtuRead(base)
    if (res?.code === 200) {
      modbusReadValue.value = String(res.data)
      ElMessage.success('读取成功')
    } else {
      modbusReadValue.value = res?.msg || '读取失败'
      ElMessage.error(modbusReadValue.value)
    }
  } catch (e: any) {
    modbusReadValue.value = e?.message || '读取异常'
    ElMessage.error(modbusReadValue.value)
  }
}
</script>

<style scoped>
.iot-monitor-container {
  padding: 20px;
}

.section-title { 
  font-weight: 600; 
  margin-bottom: 8px; 
}

.control-section { 
  margin-bottom: 8px; 
}
</style>
